Join 運算子,是 LINQ 標準查詢運算子中,應用難度較高的運算子,我盡量用範例引導的方式讓大家熟悉它。
自學筆記這系列是我自己學習的一些心得分享,歡迎指教。這系列的分享,會以 C# + 我比較熟的 Net 3.5 環境為主。
另外本系列預計至少會切成【打地基】和【語法應用】兩大部分做分享。打地基的部分,講的是 LINQ 的組成元素,這部分幾乎和 LINQ 無關,反而是 C# 2.0、C# 3.0 的一堆語言特性,例如:型別推斷、擴充方法、泛型、委派等等,不過都會把分享的範圍限制在和 LINQ 應用有直接相關功能。
PS. LINQ 自學筆記幾乎所有範例,都可直接複製到 LINQPad 4 上執行(大多是用 Statements 和 Program 模式)。因為它輕巧好用,功能強大,寫範例很方便,請大家自行到以下網址下載最新的 LINQPad:http://www.LINQpad.net/。
Join 運算子,可以讓我們把兩個來源序列聚合為一個輸出序列。Join 運算子有兩個多載方法,但是第二個多載方法 LINQ to Entities 和 LINQ to SQL 並不支援,使用時請注意:
public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>( 
    this IEnumerable<TOuter> outer, 
    IEnumerable<TInner> inner, 
    Func<TOuter, TKey> outerKeySelector, 
    Func<TInner, TKey> innerKeySelector, 
    Func<TOuter, TInner, TResult> resultSelector 
)
public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>( 
    this IEnumerable<TOuter> outer, 
    IEnumerable<TInner> inner, 
    Func<TOuter, TKey> outerKeySelector, 
    Func<TInner, TKey> innerKeySelector, 
    Func<TOuter, TInner, TResult> resultSelector, 
    IEqualityComparer<TKey> comparer 
)
Join 運算子的邏輯,大致上是這樣:
Join 運算子有幾個要注意的地方:
在看範例程式碼前,我們要先建立兩個資料來源序列,以利後續 Demo,為了方便大家使用,所以採用 LINQ to Objects 的方式處理,做資料來源的程式碼如下:
void Main() 
{ 
    var Customers = DataProvider.getCustomers(); 
    var Orders = DataProvider.getOrders(); 
    Customers.Dump(); 
    Orders.Dump(); 
}
public static class DataProvider 
{ 
    public static List<Customer> getCustomers() 
    { 
        var Customers = new List<Customer> 
        { 
            new Customer {ID = 1, Name = "Leo"}, 
            new Customer {ID = 2, Name = "Rose"}, 
            new Customer {ID = 3, Name = "Alvin"}, 
            new Customer {ID = 4, Name = "Emy"}, 
            new Customer {ID = 5, Name = "Alice"}, 
            new Customer {ID = 6, Name = "Bobo"} 
        }; 
        return Customers; 
    } 
    public static List<Order> getOrders() 
    { 
        var Orders = new List<Order> 
        { 
            new Order {ID = 1, CustomerID = 1, Date = new DateTime(2012,1,5), Description = "Mouse", Price = 480}, 
            new Order {ID = 2, CustomerID = 1, Date = new DateTime(2012,2,15), Description = "Books", Price = 880}, 
            new Order {ID = 3, CustomerID = 2, Date = new DateTime(2011,6,16), Description = "Keyboard", Price = 290}, 
            new Order {ID = 4, CustomerID = 2, Date = new DateTime(2012,3,25), Description = "NoteBook", Price = 16800}, 
            new Order {ID = 5, CustomerID = 3, Date = new DateTime(2012,8,15), Description = "Mouse", Price = 480}, 
            new Order {ID = 6, CustomerID = 4, Date = new DateTime(2011,6,22), Description = "NoteBook", Price = 16800}, 
            new Order {ID = 7, CustomerID = 4, Date = new DateTime(2011,10,10), Description = "Mouse", Price = 480}, 
            new Order {ID = 8, CustomerID = 4, Date = new DateTime(2012,9,8), Description = "Camera", Price = 29900}, 
        }; 
        return Orders; 
    } 
}
public class Customer 
{ 
    public int ID { get; set; } 
    public string Name { get; set; } 
} 
public class Order 
{ 
    public int ID { get; set; } 
    public int CustomerID { get; set; } 
    public DateTime Date { get; set; } 
    public string Description { get; set; } 
    public Decimal Price { get; set; } 
} 

準備好資料來源後,我們來看最簡單的 Join 範例:
//請注意:建立資料來源的程式碼就不重覆貼了,請自行參閱前述內容
void Main() 
{ 
    var Customers = DataProvider.getCustomers(); 
    var Orders = DataProvider.getOrders();
    var query = from c in Customers 
                join o in Orders on c.ID equals o.CustomerID 
                select c.Name + " 買了 " + o.Description; 
    query.Dump(); 
//    Customers.Join ( 
//        Orders, 
//        c => c.ID, 
//        o => o.CustomerID, 
//        (c, o) => ((c.Name + " 買了 ") + o.Description) 
//    ).Dump(); 
}

Join 運算子,在查詢表達式中,必須搭配 in、on 和 equals 三個關鍵字使用。上述範例註解的部分是對等的方法架構查詢語法。範例程式碼說明如下:
這個範例請務必了解,因為這是最簡單的版本,也是基礎樣式,若不能融會貫通,後續的範例就更難理解了。
接著我們要研究一個小題目,就是 Join 運算子和之前學過的 SelectMany 運算子,大多時候可以互相替代,以上述範例來看,可改寫成以下 SelectMany 運算子的樣式:
void Main() 
{ 
    var Customers = DataProvider.getCustomers(); 
    var Orders = DataProvider.getOrders();
//    var query = from c in Customers 
//                join o in Orders on c.ID equals o.CustomerID 
//                select c.Name + " 買了 " + o.Description; 
    var query = 
        from c in Customers
        from o in Orders where c.ID == o.CustomerID 
        select c.Name + " 買了 " + o.Description; 
    query.Dump(); 
//    Customers.SelectMany( 
//                c => Orders, 
//                (c, o) => new {c = c, o = o}) 
//             .Where (temp0 => (temp0.c.ID == temp0.o.CustomerID)) 
//             .Select (temp0 => ((temp0.c.Name + " 買了 ") + temp0.o.Description)) 
//             .Dump(); 
}
上述沒有註解的查詢表達式,用了兩個 from,並透過第二個 from 的 where 關鍵子指定相等聯結的欄位,此語法轉譯為方法架構查詢,就是後面註解起來的區塊。即然可以替代,那誰比較好呢?這問題沒有正確答案,看個人喜好就行,不過至少可以研究看看,那一種方式效能會比較好呢?
就我個人實務應用的經驗,都是 Join 比較快,當然我們要有實驗精神,所以我把上述的範例延伸為 Join v.s. SelectMany 的效能比賽,範例程式如下:
void Main() 
{ 
    var Customers = DataProvider.getCustomers(); 
    var Orders = DataProvider.getOrders(); 
    Console.WriteLine("Customer 數量:" + Customers.Count()); 
    Console.WriteLine("Orders 數量:" + Orders.Count()); 
    Stopwatch sw = new Stopwatch(); 
    sw.Start(); 
    var slowQuery = 
        from c in Customers 
        from o in Orders where c.ID == o.CustomerID 
        select c.Name + " 買了 " + o.Description; 
    Console.WriteLine("Slow query result count: " + slowQuery.Count()); 
    sw.Stop(); 
    Console.WriteLine("Slow query with SelectMany (Milliseconds): " + (sw.ElapsedMilliseconds)); 
    sw.Restart(); 
    var fastQuery = 
        from c in Customers 
        join o in Orders on c.ID equals o.CustomerID 
        select c.Name + " 買了 " + o.Description; 
    Console.WriteLine("Fast query result count: " + fastQuery.Count()); 
    sw.Stop(); 
    Console.WriteLine("Fast query with Join (Milliseconds): " + (sw.ElapsedMilliseconds)); 
}
/* 輸出:
Customer 數量:10000 
Orders 數量:12000 
Slow query result count: 10008 
Slow query with SelectMany (Milliseconds): 16199 
Fast query result count: 10008 
Fast query with Join (Milliseconds): 9
*/
為了效能測試,我微調了產生 Customers、Orders 的程式,一次可產出如上圖 10,000 和 12,000 筆資料,且有 10,008 筆資料可以聯結起來(故意不做完全對應),最後輸出兩種運算子的查詢結果筆數及所花費的時間,證明相同的查詢結果,Join 運算子在大量資料查詢時,效能遠勝 SelectMany 運算子。
其實這個結果要說明差異點很簡單,從兩種運算子的方法架構查詢語法即可一窺究竟:
Customers.SelectMany (c => Orders, (c, o) => new {c = c, o = o}) 
         .Where (temp0 => (temp0.c.ID == temp0.o.CustomerID)) 
         .Select (temp0 => ((temp0.c.Name + " 買了 ") + temp0.o.Description)) 
 
Customers.Join ( 
      Orders, 
      c => c.ID, 
      o => o.CustomerID, 
      (c, o) => ((c.Name + " 買了 ") + o.Description) 
   ) 
最重要的差異就是,SelectMany 運算子一開始要先把 Customers 和 Orders 中所有資料全部展開,產生新的 IEnumerable<匿名型別> 序列,然後才進行 Where 條件過濾,最後才整理出要回傳的 TResult。也就是說,為了取回我們要的結果,它必須產生三個 IEnumerable 序列才能得到結果,記憶體的消耗極大,當然就快不起來。
Join 運算子不一樣,如同文章開頭的敘述,它一開始先列舉 inner 中所有項目,將主鍵存到 Hashtable 中,然後逐一列舉 outer 中的項目,拿 outer 的主鍵到 Hashtable 中對應,有符合就整理成要輸出的匿名型別資料,並指定到 IEnumerable<TResult> 中。兩種運算子執行作業的方式大不相同,當然也就造成效能表現上的不同。